namespace FormStateKeeper
{
    using System;
    using System.Security.Permissions;
    using System.Security.Principal;
    using System.Web;
    using System.Web.Security;

    /// <summary>
    /// This HttpModule captures the form contents and stores it if the following is true:
    /// 1. The user is submitting a form.
    /// 2. The user's forms authentication ticket has timed out.
    /// </summary>
    [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    public class FormStateKeeperModule : BaseHttpModule
    {
        internal string CookieName = "#FormStateKeeperModule/FormRestoreId";
        private static readonly IPrincipal _anonymousUser = new GenericPrincipal(new GenericIdentity(string.Empty), null);
        private string _formsAuthCookieName;
        private string _loginUrl;

        /// <summary>
        /// Initializes the FormStateKeeperModule, read web environment details and wire up events.
        /// </summary>
        /// <param name="context">Active HttpContext.</param>
        public override void OnInit(HttpApplication context)
        {
            _loginUrl = FormsAuthentication.LoginUrl;
            this._formsAuthCookieName = FormsAuthentication.FormsCookieName;

            if (!string.IsNullOrEmpty(_loginUrl))
            {
                int index = _loginUrl.IndexOf('?');
                if (index != -1)
                    _loginUrl = _loginUrl.Substring(0, index);
            }
        }

        /// <summary>
        /// Captures the form contents if forms authentication ticket is missing while making
        /// a HTTP POST (typically submit from a form).
        /// Restores the form contents if the form contents were previously stored.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <remarks>
        /// We have to hook up with the BeginRequest, as this is the only point in the event pipeline
        /// where the Request.Form is still accessible and not empty.
        /// </remarks>
        public override void OnBeginRequest(HttpContextBase context)
        {
            // Save the form state if making a HTTP POST with an expired forms authentication ticket.
            if (IsPost(context) && !IsAccessingLoginPage(context, _loginUrl))
            {
                HttpCookie cookie = context.Request.Cookies[this._formsAuthCookieName];
                if (cookie != null)
                {
                    try
                    {
                        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
                        if (ticket != null && ticket.Expired && !HasAnonymousAccess(context))
                        {
                            // Create a FormState instance with the current form post contents and store a 'pointer'
                            // to this instance in a cookie, so we can find it again after relogin.
                            FormState state = FormState.Create(context);
                            if (state != null)
                            {
                                cookie = new HttpCookie(CookieName, state.StateId);
                                cookie.HttpOnly = true;
                                context.Response.Cookies.Add(cookie);
                            }
                        }
                    }
                    catch (ArgumentException)
                    {
                    }
                }
            }
        }

        /// <summary>
        /// Use our special handler to restore the form contents.
        /// </summary>
        /// <param name="context">The context.</param>
        public override void OnPostMapRequestHandler(HttpContextBase context)
        {
            HttpCookie cookie = context.Request.Cookies[CookieName];

            // See if any stored form state is available and activate the handler.
            if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
            {
                FormState state = FormState.Load(cookie.Value);
                if (state != null && string.Equals(state.Path, context.Request.Path, StringComparison.OrdinalIgnoreCase) && state.Form != null && state.Form.Count != 0)
                {
                    //FormState.SetCurrent(context, state); // Attach the retrieved form state to the current request
                    context.Handler = new FormStateRestoreHandler(state);
                }
            }
        }

        /// <summary>
        /// Overload to use mainly when testing - does not require an HttpApplication parameter
        /// and allows to set the loginUrl and formsAuthCookieName.
        /// </summary>
        /// <param name="loginUrl">The login URL.</param>
        /// <param name="formsAuthCookieName">Name of the forms cookie.</param>
        internal void OnInit(string loginUrl, string formsAuthCookieName)
        {
            _loginUrl = loginUrl;
            this._formsAuthCookieName = formsAuthCookieName;
        }

        /// <summary>
        /// Checks if this request is a http post.
        /// </summary>
        /// <param name="context">The active http context.</param>
        /// <returns>
        /// <c>true</c> if the specified context's request is a post; otherwise, <c>false</c>.
        /// </returns>
        protected static bool IsPost(HttpContextBase context)
        {
            return string.Equals("POST", context.Request.HttpMethod, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Checks if the current request is hitting the login page.
        /// </summary>
        /// <param name="context">The current http context.</param>
        /// <param name="loginUrl">The login page url.</param>
        /// <returns>True if the supplied request is hitting the supplied login page, otherwise false.</returns>
        protected static bool IsAccessingLoginPage(HttpContextBase context, string loginUrl)
        {
            bool result = false;

            // Check if the current url is actually the login page url - and handle different urlencoding issues
            if (!string.IsNullOrEmpty(loginUrl))
            {
                if (string.Equals(context.Request.Path, loginUrl, StringComparison.OrdinalIgnoreCase))
                {
                    result = true;
                }
                else if (loginUrl.IndexOf('%') != -1)
                {
                    string temp = HttpUtility.UrlDecode(loginUrl);
                    if (string.Equals(context.Request.Path, temp, StringComparison.OrdinalIgnoreCase))
                    {
                        result = true;
                    }
                    else
                    {
                        temp = HttpUtility.UrlDecode(loginUrl, context.Request.ContentEncoding);
                        if (string.Equals(context.Request.Path, temp, StringComparison.OrdinalIgnoreCase))
                        {
                            result = true;
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Checks if the current request is hitting a page that has anonymous access.
        /// </summary>
        /// <param name="context">The current http context.</param>
        /// <returns>True if the request hits anonoymous access, otherwise false.</returns>
        protected virtual bool HasAnonymousAccess(HttpContextBase context)
        {
            return UrlAuthorizationModule.CheckUrlAccessForPrincipal(
                context.Request.Path,
                _anonymousUser,
                context.Request.HttpMethod);
        }
    }
}